/**
 * @preserve XDate v@VERSION
 * Docs & Licensing: http://arshaw.com/xdate/
 */

/*
 * Internal Architecture
 * ---------------------
 * An XDate wraps a native Date. The native Date is stored in the '0' property of the object.
 * UTC-mode is determined by whether the internal native Date's toString method is set to
 * Date.prototype.toUTCString (see getUTCMode).
 *
 */

var XDate = (function(Date, Math, Array, undefined) {

    /** @const */
    var FULLYEAR = 0;
    /** @const */
    var MONTH = 1;
    /** @const */
    var DATE = 2;
    /** @const */
    var HOURS = 3;
    /** @const */
    var MINUTES = 4;
    /** @const */
    var SECONDS = 5;
    /** @const */
    var MILLISECONDS = 6;
    /** @const */
    var DAY = 7;
    /** @const */
    var YEAR = 8;
    /** @const */
    var WEEK = 9;
    /** @const */
    var DAY_MS = 86400000;
    var ISO_FORMAT_STRING = "yyyy-MM-dd'T'HH:mm:ss(.fff)";
    var ISO_FORMAT_STRING_TZ = ISO_FORMAT_STRING + "zzz";

    var methodSubjects = ['FullYear', // 0
    'Month', // 1
    'Date', // 2
    'Hours', // 3
    'Minutes', // 4
    'Seconds', // 5
    'Milliseconds', // 6
    'Day', // 7
    'Year' // 8
    ];
    var subjectPlurals = ['Years', // 0
    'Months', // 1
    'Days' // 2
    ];
    var unitsWithin = [12, // months in year
    31, // days in month (sort of)
    24, // hours in day
    60, // minutes in hour
    60, // seconds in minute
    1000, // milliseconds in second
    1 //
    ];
    var formatStringRE = new RegExp("(([a-zA-Z])\\2*)|" + // 1, 2
    "(\\(" + "(('.*?'|\\(.*?\\)|.)*?)" + "\\))|" + // 3, 4, 5 (allows for 1 level of inner quotes or parens)
    "('(.*?)')" // 6, 7
    );
    var UTC = Date.UTC;
    var toUTCString = Date.prototype.toUTCString;
    var proto = XDate.prototype;

    // This makes an XDate look pretty in Firebug and Web Inspector.
    // It makes an XDate seem array-like, and displays [ <internal-date>.toString() ]
    proto.length = 1;
    proto.splice = Array.prototype.splice;

    /* Constructor
    ---------------------------------------------------------------------------------*/

    // TODO: in future, I'd change signature for the constructor regarding the `true` utc-mode param. ~ashaw
    //   I'd move the boolean to be the *first* argument. Still optional. Seems cleaner.
    //   I'd remove it from the `xdate`, `nativeDate`, and `milliseconds` constructors.
    //      (because you can simply call .setUTCMode(true) after)
    //   And I'd only leave it for the y/m/d/h/m/s/m and `dateString` constructors
    //      (because those are the only constructors that need it for DST-gap data-loss reasons)
    //   Should do this for 1.0

    function XDate() {
        return init((this instanceof XDate) ? this : new XDate(), arguments);
    }

    function init(xdate, args) {
        var len = args.length;
        var utcMode;
        if (isBoolean(args[len - 1])) {
            utcMode = args[--len];
            args = slice(args, 0, len);
        }
        if (!len) {
            xdate[0] = new Date();
        } else if (len == 1) {
            var arg = args[0];
            if ( arg instanceof Date || isNumber(arg)) {
                xdate[0] = new Date(+arg);
            } else if ( arg instanceof XDate) {
                xdate[0] = _clone(arg);
            } else if (isString(arg)) {
                xdate[0] = new Date(0);
                xdate = parse(arg, utcMode || false, xdate);
            }
        } else {
            xdate[0] = new Date(UTC.apply(Date, args));
            if (!utcMode) {
                xdate[0] = coerceToLocal(xdate[0]);
            }
        }
        if (isBoolean(utcMode)) {
            setUTCMode(xdate, utcMode);
        }
        return xdate;
    }

    /* UTC Mode Methods
     ---------------------------------------------------------------------------------*/

    proto.getUTCMode = methodize(getUTCMode);
    function getUTCMode(xdate) {
        return xdate[0].toString === toUTCString;
    };

    proto.setUTCMode = methodize(setUTCMode);
    function setUTCMode(xdate, utcMode, doCoercion) {
        if (utcMode) {
            if (!getUTCMode(xdate)) {
                if (doCoercion) {
                    xdate[0] = coerceToUTC(xdate[0]);
                }
                xdate[0].toString = toUTCString;
            }
        } else {
            if (getUTCMode(xdate)) {
                if (doCoercion) {
                    xdate[0] = coerceToLocal(xdate[0]);
                } else {
                    xdate[0] = new Date(+xdate[0]);
                }
                // toString will have been cleared
            }
        }
        return xdate;
        // for chaining
    }


    proto.getTimezoneOffset = function() {
        if (getUTCMode(this)) {
            return 0;
        } else {
            return this[0].getTimezoneOffset();
        }
    };

    /* get / set / add / diff Methods (except for week-related)
     ---------------------------------------------------------------------------------*/

    each(methodSubjects, function(subject, fieldIndex) {

        proto['get' + subject] = function() {
            return _getField(this[0], getUTCMode(this), fieldIndex);
        };

        if (fieldIndex != YEAR) {// because there is no getUTCYear

            proto['getUTC' + subject] = function() {
                return _getField(this[0], true, fieldIndex);
            };

        }

        if (fieldIndex != DAY) {// because there is no setDay or setUTCDay
            // and the add* and diff* methods use DATE instead

            proto['set' + subject] = function(value) {
                _set(this, fieldIndex, value, arguments, getUTCMode(this));
                return this;
                // for chaining
            };

            if (fieldIndex != YEAR) {// because there is no setUTCYear
                // and the add* and diff* methods use FULLYEAR instead

                proto['setUTC' + subject] = function(value) {
                    _set(this, fieldIndex, value, arguments, true);
                    return this;
                    // for chaining
                };

                proto['add' + (subjectPlurals[fieldIndex] || subject)] = function(delta, preventOverflow) {
                    _add(this, fieldIndex, delta, preventOverflow);
                    return this;
                    // for chaining
                };

                proto['diff' + (subjectPlurals[fieldIndex] || subject)] = function(otherDate) {
                    return _diff(this, otherDate, fieldIndex);
                };

            }

        }

    });

    function _set(xdate, fieldIndex, value, args, useUTC) {
        var getField = curry(_getField, xdate[0], useUTC);
        var setField = curry(_setField, xdate[0], useUTC);
        var expectedMonth;
        var preventOverflow = false;
        if (args.length == 2 && isBoolean(args[1])) {
            preventOverflow = args[1];
            args = [value];
        }
        if (fieldIndex == MONTH) {
            expectedMonth = (value % 12 + 12) % 12;
        } else {
            expectedMonth = getField(MONTH);
        }
        setField(fieldIndex, args);
        if (preventOverflow && getField(MONTH) != expectedMonth) {
            setField(MONTH, [getField(MONTH) - 1]);
            setField(DATE, [getDaysInMonth(getField(FULLYEAR), getField(MONTH))]);
        }
    }

    function _add(xdate, fieldIndex, delta, preventOverflow) {
        delta = Number(delta);
        var intDelta = Math.floor(delta);
        xdate['set' + methodSubjects[fieldIndex]](xdate['get' + methodSubjects[fieldIndex]]() + intDelta, preventOverflow || false);
        if (intDelta != delta && fieldIndex < MILLISECONDS) {
            _add(xdate, fieldIndex + 1, (delta - intDelta) * unitsWithin[fieldIndex], preventOverflow);
        }
    }

    function _diff(xdate1, xdate2, fieldIndex) {// fieldIndex=FULLYEAR is for years, fieldIndex=DATE is for days
        xdate1 = xdate1.clone().setUTCMode(true, true);
        xdate2 = XDate(xdate2).setUTCMode(true, true);
        var v = 0;
        if (fieldIndex == FULLYEAR || fieldIndex == MONTH) {
            for (var i = MILLISECONDS, methodName; i >= fieldIndex; i--) {
                v /= unitsWithin[i];
                v += _getField(xdate2, false, i) - _getField(xdate1, false, i);
            }
            if (fieldIndex == MONTH) {
                v += (xdate2.getFullYear() - xdate1.getFullYear()) * 12;
            }
        } else if (fieldIndex == DATE) {
            var clear1 = xdate1.toDate().setUTCHours(0, 0, 0, 0);
            // returns an ms value
            var clear2 = xdate2.toDate().setUTCHours(0, 0, 0, 0);
            // returns an ms value
            v = Math.round((clear2 - clear1) / DAY_MS) + ((xdate2 - clear2) - (xdate1 - clear1)) / DAY_MS;
        } else {
            v = (xdate2 - xdate1) / [
            3600000, // milliseconds in hour
            60000,   // milliseconds in minute
            1000,    // milliseconds in second
            1        //
            ][fieldIndex - 3];
        }
        return v;
    }

    /* Week Methods
     ---------------------------------------------------------------------------------*/

    proto.getWeek = function() {
        return _getWeek(curry(_getField, this, false));
    };

    proto.getUTCWeek = function() {
        return _getWeek(curry(_getField, this, true));
    };

    proto.setWeek = function(n, year) {
        _setWeek(this, n, year, false);
        return this;
        // for chaining
    };

    proto.setUTCWeek = function(n, year) {
        _setWeek(this, n, year, true);
        return this;
        // for chaining
    };

    proto.addWeeks = function(delta) {
        return this.addDays(Number(delta) * 7);
    };

    proto.diffWeeks = function(otherDate) {
        return _diff(this, otherDate, DATE) / 7;
    };

    function _getWeek(getField) {
        return getWeek(getField(FULLYEAR), getField(MONTH), getField(DATE));
    }

    function getWeek(year, month, date) {
        var d = new Date(UTC(year, month, date));
        var week1 = getWeek1(getWeekYear(year, month, date));
        return Math.floor(Math.round((d - week1) / DAY_MS) / 7) + 1;
    }

    function getWeekYear(year, month, date) {// get the year that the date's week # belongs to
        var d = new Date(UTC(year, month, date));
        if (d < getWeek1(year)) {
            return year - 1;
        } else if (d >= getWeek1(year + 1)) {
            return year + 1;
        }
        return year;
    }

    function getWeek1(year) {// returns Date of first week of year, in UTC
        var d = new Date(UTC(year, 0, 4));
        d.setUTCDate(d.getUTCDate() - (d.getUTCDay() + 6) % 7);
        // make it Monday of the week
        return d;
    }

    function _setWeek(xdate, n, year, useUTC) {
        var getField = curry(_getField, xdate, useUTC);
        var setField = curry(_setField, xdate, useUTC);

        if (year === undefined) {
            year = getWeekYear(getField(FULLYEAR), getField(MONTH), getField(DATE));
        }

        var week1 = getWeek1(year);
        if (!useUTC) {
            week1 = coerceToLocal(week1);
        }

        xdate.setTime(+week1);
        setField(DATE, [getField(DATE) + (n - 1) * 7]);
        // would have used xdate.addUTCWeeks :(
        // n-1 because n is 1-based
    }

    /* Parsing
     ---------------------------------------------------------------------------------*/

    XDate.parsers = [parseISO];

    XDate.parse = function(str) {
        return +XDate('' + str);
    };

    function parse(str, utcMode, xdate) {
        var parsers = XDate.parsers;
        var i = 0;
        var res;
        for (; i < parsers.length; i++) {
            res = parsers[i](str, utcMode, xdate);
            if (res) {
                return res;
            }
        }
        xdate[0] = new Date(str);
        return xdate;
    }

    function parseISO(str, utcMode, xdate) {
        var m = str.match(/^(\d{4})(-(\d{2})(-(\d{2})([T ](\d{2}):(\d{2})(:(\d{2})(\.(\d+))?)?(Z|(([-+])(\d{2})(:?(\d{2}))?))?)?)?)?$/);
        if (m) {
            var d = new Date(UTC(m[1], m[3] ? m[3] - 1 : 0, m[5] || 1, m[7] || 0, m[8] || 0, m[10] || 0, m[12] ? Number('0.' + m[12]) * 1000 : 0));
            if (m[13]) {// has gmt offset or Z
                if (m[14]) {// has gmt offset
                    d.setUTCMinutes(d.getUTCMinutes() + (m[15] == '-' ? 1 : -1) * (Number(m[16]) * 60 + (m[18] ? Number(m[18]) : 0)));
                }
            } else {// no specified timezone
                if (!utcMode) {
                    d = coerceToLocal(d);
                }
            }
            return xdate.setTime(+d);
        }
    }

    /* Formatting
     ---------------------------------------------------------------------------------*/

    proto.toString = function(formatString, settings, uniqueness) {
        if (formatString === undefined || !valid(this)) {
            return this[0].toString();
            // already accounts for utc-mode (might be toUTCString)
        } else {
            return format(this, formatString, settings, uniqueness, getUTCMode(this));
        }
    };

    proto.toUTCString = proto.toGMTString = function(formatString, settings, uniqueness) {
        if (formatString === undefined || !valid(this)) {
            return this[0].toUTCString();
        } else {
            return format(this, formatString, settings, uniqueness, true);
        }
    };

    proto.toISOString = function() {
        return this.toUTCString(ISO_FORMAT_STRING_TZ);
    };

    XDate.defaultLocale = '';
    XDate.locales = {
        '' : {
            monthNames : ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
            monthNamesShort : ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
            dayNames : ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
            dayNamesShort : ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
            amDesignator : 'AM',
            pmDesignator : 'PM'
        }
    };
    XDate.formatters = {
        i : ISO_FORMAT_STRING,
        u : ISO_FORMAT_STRING_TZ
    };

    function format(xdate, formatString, settings, uniqueness, useUTC) {

        var locales = XDate.locales;
        var defaultLocaleSettings = locales[XDate.defaultLocale] || {};
        var getField = curry(_getField, xdate, useUTC);

        settings = (isString(settings) ? locales[settings] : settings) || {};

        function getSetting(name) {
            return settings[name] || defaultLocaleSettings[name];
        }

        function getFieldAndTrace(fieldIndex) {
            if (uniqueness) {
                var i = (fieldIndex == DAY ? DATE : fieldIndex) - 1;
                for (; i >= 0; i--) {
                    uniqueness.push(getField(i));
                }
            }
            return getField(fieldIndex);
        }

        return _format(xdate, formatString, getFieldAndTrace, getSetting, useUTC);
    }

    function _format(xdate, formatString, getField, getSetting, useUTC) {
        var m;
        var subout;
        var out = '';
        while ( m = formatString.match(formatStringRE)) {
            out += formatString.substr(0, m.index);
            if (m[1]) {// consecutive alphabetic characters
                out += processTokenString(xdate, m[1], getField, getSetting, useUTC);
            } else if (m[3]) {// parenthesis
                subout = _format(xdate, m[4], getField, getSetting, useUTC);
                if (parseInt(subout.replace(/\D/g, ''), 10)) {// if any of the numbers are non-zero. or no numbers at all
                    out += subout;
                }
            } else {// else if (m[6]) { // single quotes
                out += m[7] || "'";
                // if inner is blank, meaning 2 consecutive quotes = literal single quote
            }
            formatString = formatString.substr(m.index + m[0].length);
        }
        return out + formatString;
    }

    function processTokenString(xdate, tokenString, getField, getSetting, useUTC) {
        var end = tokenString.length;
        var replacement;
        var out = '';
        while (end > 0) {
            replacement = getTokenReplacement(xdate, tokenString.substr(0, end), getField, getSetting, useUTC);
            if (replacement !== undefined) {
                out += replacement;
                tokenString = tokenString.substr(end);
                end = tokenString.length;
            } else {
                end--;
            }
        }
        return out + tokenString;
    }

    function getTokenReplacement(xdate, token, getField, getSetting, useUTC) {
        var formatter = XDate.formatters[token];
        if (isString(formatter)) {
            return _format(xdate, formatter, getField, getSetting, useUTC);
        } else if (isFunction(formatter)) {
            return formatter(xdate, useUTC || false, getSetting);
        }
        switch (token) {
        case 'fff'  :
            return zeroPad(getField(MILLISECONDS), 3);
        case 's'    :
            return getField(SECONDS);
        case 'ss'   :
            return zeroPad(getField(SECONDS));
        case 'm'    :
            return getField(MINUTES);
        case 'mm'   :
            return zeroPad(getField(MINUTES));
        case 'h'    :
            return getField(HOURS) % 12 || 12;
        case 'hh'   :
            return zeroPad(getField(HOURS) % 12 || 12);
        case 'H'    :
            return getField(HOURS);
        case 'HH'   :
            return zeroPad(getField(HOURS));
        case 'd'    :
            return getField(DATE);
        case 'dd'   :
            return zeroPad(getField(DATE));
        case 'ddd'  :
            return getSetting('dayNamesShort')[getField(DAY)] || '';
        case 'dddd' :
            return getSetting('dayNames')[getField(DAY)] || '';
        case 'M'    :
            return getField(MONTH) + 1;
        case 'MM'   :
            return zeroPad(getField(MONTH) + 1);
        case 'MMM'  :
            return getSetting('monthNamesShort')[getField(MONTH)] || '';
        case 'MMMM' :
            return getSetting('monthNames')[getField(MONTH)] || '';
        case 'yy'   :
            return (getField(FULLYEAR) + '').substring(2);
        case 'yyyy' :
            return getField(FULLYEAR);
        case 't'    :
            return _getDesignator(getField, getSetting).substr(0, 1).toLowerCase();
        case 'tt'   :
            return _getDesignator(getField, getSetting).toLowerCase();
        case 'T'    :
            return _getDesignator(getField, getSetting).substr(0, 1);
        case 'TT'   :
            return _getDesignator(getField, getSetting);
        case 'z'    :
        case 'zz'   :
        case 'zzz'  :
            return useUTC ? 'Z' : _getTZString(xdate, token);
        case 'w'    :
            return _getWeek(getField);
        case 'ww'   :
            return zeroPad(_getWeek(getField));
        case 'S'    :
            var d = getField(DATE);
            if (d > 10 && d < 20)
                return 'th';
            return ['st', 'nd', 'rd'][d % 10 - 1] || 'th';
        }
    }

    function _getTZString(xdate, token) {
        var tzo = xdate.getTimezoneOffset();
        var sign = tzo < 0 ? '+' : '-';
        var hours = Math.floor(Math.abs(tzo) / 60);
        var minutes = Math.abs(tzo) % 60;
        var out = hours;
        if (token == 'zz') {
            out = zeroPad(hours);
        } else if (token == 'zzz') {
            out = zeroPad(hours) + ':' + zeroPad(minutes);
        }
        return sign + out;
    }

    function _getDesignator(getField, getSetting) {
        return getField(HOURS) < 12 ? getSetting('amDesignator') : getSetting('pmDesignator');
    }

    /* Misc Methods
     ---------------------------------------------------------------------------------*/

    each([// other getters
    'getTime', 'valueOf', 'toDateString', 'toTimeString', 'toLocaleString', 'toLocaleDateString', 'toLocaleTimeString', 'toJSON'], function(methodName) {
        proto[methodName] = function() {
            return this[0][methodName]();
        };
    });

    proto.setTime = function(t) {
        this[0].setTime(t);
        return this;
        // for chaining
    };

    proto.valid = methodize(valid);
    function valid(xdate) {
        return !isNaN(+xdate[0]);
    }


    proto.clone = function() {
        return new XDate(this);
    };

    proto.clearTime = function() {
        return this.setHours(0, 0, 0, 0);
        // will return an XDate for chaining
    };

    proto.toDate = function() {
        return new Date(+this[0]);
    };

    /* Misc Class Methods
     ---------------------------------------------------------------------------------*/

    XDate.now = function() {
        return +new Date();
    };

    XDate.today = function() {
        return new XDate().clearTime();
    };

    XDate.UTC = UTC;

    XDate.getDaysInMonth = getDaysInMonth;

    /* Internal Utilities
     ---------------------------------------------------------------------------------*/

    function _clone(xdate) {// returns the internal Date object that should be used
        var d = new Date(+xdate[0]);
        if (getUTCMode(xdate)) {
            d.toString = toUTCString;
        }
        return d;
    }

    function _getField(d, useUTC, fieldIndex) {
        return d['get' + (useUTC ? 'UTC' : '') + methodSubjects[fieldIndex]]();
    }

    function _setField(d, useUTC, fieldIndex, args) {
        d['set' + ( useUTC ? 'UTC' : '') + methodSubjects[fieldIndex]].apply(d, args);
    }

    /* Date Math Utilities
     ---------------------------------------------------------------------------------*/

    function coerceToUTC(date) {
        return new Date(UTC(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds()));
    }

    function coerceToLocal(date) {
        return new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds(), date.getUTCMilliseconds());
    }

    function getDaysInMonth(year, month) {
        return 32 - new Date(UTC(year, month, 32)).getUTCDate();
    }

    /* General Utilities
     ---------------------------------------------------------------------------------*/

    function methodize(f) {
        return function() {
            return f.apply(undefined, [this].concat(slice(arguments)));
        };
    }

    function curry(f) {
        var firstArgs = slice(arguments, 1);
        return function() {
            return f.apply(undefined, firstArgs.concat(slice(arguments)));
        };
    }

    function slice(a, start, end) {
        return Array.prototype.slice.call(a, start || 0, // start and end cannot be undefined for IE
        end === undefined ? a.length : end);
    }

    function each(a, f) {
        for (var i = 0; i < a.length; i++) {
            f(a[i], i);
        };
    }

    function isString(arg) {
        return typeof arg == 'string';
    }

    function isNumber(arg) {
        return typeof arg == 'number';
    }

    function isBoolean(arg) {
        return typeof arg == 'boolean';
    }

    function isFunction(arg) {
        return typeof arg == 'function';
    }

    function zeroPad(n, len) {
        len = len || 2;
        n += '';
        while (n.length < len) {
            n = '0' + n;
        }
        return n;
    }

    // Export for Node.js
    if ( typeof module !== 'undefined' && module.exports) {
        module.exports = XDate;
    }

    // AMD
    if ( typeof define === 'function' && define.amd) {
        define([], function() {
            return XDate;
        });
    }

    return XDate;

})(Date, Math, Array);

window.XDate = XDate;
